Skip to content

Add ability to set thread affinity#51

Closed
esyr wants to merge 10 commits into
openssl:mainfrom
esyr:esyr/thread-affinity
Closed

Add ability to set thread affinity#51
esyr wants to merge 10 commits into
openssl:mainfrom
esyr:esyr/thread-affinity

Conversation

@esyr

@esyr esyr commented Sep 25, 2025

Copy link
Copy Markdown
Member

Includes the relevant update to the pkeyread test, as it already tries to report some thread indices in the -v mode.

@esyr esyr requested review from Sashan and jogme September 25, 2025 23:53

@Sashan Sashan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good I could find just few nits in threads.c you might want to address.
thanks.

Comment thread source/perflib/threads.c Outdated
unsigned int ret = 0;

for (size_t i = 0; i < sizeof(a) * CHAR_BIT; i++)
ret += ((a & (1ULL << i)) == 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this what I've messed up in my sgguested change. we discussed this off-llist we are supposed to count bits which are set, right? if so then we need ret += ((a & (1ULL << i)) != 0); here.

Comment thread source/perflib/threads.c
goto err;
}

ta = OPENSSL_malloc(sizeof(*ta) * threadcount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is good to use OPENSSL_malloc() so tests work with libraries which don't provide OPENSSL_malloc_array()

Comment thread source/perflib/threads.c
args[i].num = i;
perflib_run_thread(&threads[i], &args[i]);
if (!(run_threads[i] = perflib_run_thread_(&threads[i], &args[i],
ta + i)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use &ta[i] here so it is clear we work with array, thanks.

@esyr esyr force-pushed the esyr/thread-affinity branch 2 times, most recently from efab928 to c033606 Compare October 16, 2025 12:18
esyr and others added 8 commits October 16, 2025 15:15
Signed-off-by: Eugene Syromiatnikov <esyr@openssl.org>
Signed-off-by: Eugene Syromiatnikov <esyr@openssl.org>
Signed-off-by: Eugene Syromiatnikov <esyr@openssl.org>
Co-Authored-by: Alexandr Nedvedicky <sashan@openssl.org>
Signed-off-by: Eugene Syromiatnikov <esyr@openssl.org>
Signed-off-by: Eugene Syromiatnikov <esyr@openssl.org>
Signed-off-by: Eugene Syromiatnikov <esyr@openssl.org>
Signed-off-by: Eugene Syromiatnikov <esyr@openssl.org>
Signed-off-by: Eugene Syromiatnikov <esyr@openssl.org>
@esyr esyr force-pushed the esyr/thread-affinity branch from c033606 to 30f2a1f Compare October 16, 2025 13:16
@esyr esyr requested a review from Sashan October 16, 2025 13:30
@esyr esyr marked this pull request as ready for review October 16, 2025 13:31
Comment thread source/perflib/threads.c
static ossl_inline unsigned int popcount(affinity_t a)
{
return __builtin_popcountl(a);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need to special case the ability to use a compiler built in here? It seems like the balance between the ifdeffery here and a single function that counts up to sizeof(unsigned long) * 8 bits is biased in favor of just having one function.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't like the idea of rolling own implementation when the built-in is right here, but I don't really care here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to use compiler built-ins can we also enable them for clang?

diff --git a/source/perflib/threads.c b/source/perflib/threads.c
index 8cf3a76..4f9187c 100644
--- a/source/perflib/threads.c
+++ b/source/perflib/threads.c
@@ -22,7 +22,7 @@
 /** affinity_t-typed value with nth bit set. */
 #define AFFINITY_BIT(n) ((affinity_t)1U << (n))
 
-#if defined(__GNUC__)
+#if defined(__GNUC__) || defined(__clang__)
 
 static ossl_inline unsigned int popcount(affinity_t a)
 {
@@ -41,7 +41,7 @@ static ossl_inline unsigned int popcount(affinity_t a)
     return ret;
 }
 
-#endif /* __GNUC__ */
+#endif /* __GNUC__ or __clang__ */
 
 int perflib_roundrobin_affinity(affinity_t *cpu_set_bits, size_t cpu_set_size,
                                 size_t num, size_t cnt, void *arg)

to be honest I'm with Neal here. My reasoning is the peftools need to be portable to as many platforms/compilers as (conveniently) possible. you are rolling the builtin implementation anway so using a bultinn one here does not buy as much.

on the other hand if limit ourselves to clang and GCC tools, then I'm fine with going to bultin only one.

the true reason I don't like the if/else here is it leaves a dead/untested code behind. In my opinion the true choice here should be:

  • being portable, then roll your own
    or
  • let's rely on compiler then code will work on platforms where bultiin is provided

in my view the perftools are roll your own case.

@bob-beck bob-beck Oct 23, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what's the goal and the problem we are seeking to solve here that we need code to support to solve.

How will we support this, when people start fiddling with it and complain about impact because they do it poorly, and attempt to be smarter than the scheduler and do it badly.

To ask this another way, if our goal is to have less noisy data for falling-down-the-hill performance tests, does this belong in testing support for us and not in the main library?

mea culpa, that's what this is, so yeah it's in the test library, I have less objections.

Comment thread source/pkeyread.c
"\t-v verbose output, includes min, max, stddev, and median times\n"
"\t-T timeout for each test run in seconds, can be fractional"
"\t-T timeout for each test run in seconds, can be fractional\n"
"\t-b Set CPU affinity for the threads (in round robin fashion)\n"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about adding this option to all the other tests in the repo?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was prototyping on pkeyread, but, yeah, adding it to other tests should be trivial.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand Nikola's question better now. and I think he is making a good point. let me ask the question different way: what is a difference between running the test using the command:

./pkeyread -f all -k all -b 16

and

taskset  0xffff ./pkeyread -f all -k all 16

If I understand things right, then th -b is a shortcut so people don't need to think of using a taskset(1) is my understanding correct?

Comment thread source/pkeyread.c
OSSL_TIME max_time;

int err = 0;
int error = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? Theres a good portion of this PR dedicated to renaming variables that doesn't really have anything to do with the addition of thread affinity management.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this address linker issues. there is function err() which conflicts with variables err. the changes in this PR just discovered this conflict. so the change got included here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't it be more prudent to just rename the err() function that commit 953a33f introduced then?

Though I'm surprised that a reasonable compiler can't tell the difference between a variable reference and a function call of the same name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't it be more prudent to just rename the err() function that commit 953a33f introduced then?

Though I'm surprised that a reasonable compiler can't tell the difference between a variable reference and a function call of the same name.

the err() is a windows version of err(3) which is commonly used on *nix it would make me sad to see it go,


#include <string.h>
#include <openssl/crypto.h>
#include <openssl/macros.h>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed? There is no other change in this file

Comment thread source/perflib/err.c
}

void
err(int status, const char *fmt, ...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why to duplicate errx function? Same for warn and warnx

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err/warn append the output of perror() to the message, while errx/warnx just print the provided string (along with the program name as a prefix).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err/warn append the output of perror() to the message, while errx/warnx just print the provided string (along with the program name as a prefix).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now; sorry for the noise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow why we need to duplicate all this stuff for thread affinity.. This seems unrelated.

If we want this it should probably be done separately, or we should ask ourselves why we can't use the standard stuff.

Comment thread source/perflib/err.h
# include <err.h>

# else /* _WIN32 */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use new lines around include in #if.

@npajkovsky

Copy link
Copy Markdown

The work is ok, but I'm a little bit lost why the work is needed.

@Sashan

Sashan commented Oct 17, 2025

Copy link
Copy Markdown
Contributor

The work is ok, but I'm a little bit lost why the work is needed.

my understanding is you want to pin a thread to CPU so scheduler does not migrate the thread which runs performance test around the system. I think this does not present on system with low number of cores. it becomes more important on large multicore systems.

Comment thread source/perflib/threads.c
static ossl_inline unsigned int popcount(affinity_t a)
{
return __builtin_popcountl(a);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to use compiler built-ins can we also enable them for clang?

diff --git a/source/perflib/threads.c b/source/perflib/threads.c
index 8cf3a76..4f9187c 100644
--- a/source/perflib/threads.c
+++ b/source/perflib/threads.c
@@ -22,7 +22,7 @@
 /** affinity_t-typed value with nth bit set. */
 #define AFFINITY_BIT(n) ((affinity_t)1U << (n))
 
-#if defined(__GNUC__)
+#if defined(__GNUC__) || defined(__clang__)
 
 static ossl_inline unsigned int popcount(affinity_t a)
 {
@@ -41,7 +41,7 @@ static ossl_inline unsigned int popcount(affinity_t a)
     return ret;
 }
 
-#endif /* __GNUC__ */
+#endif /* __GNUC__ or __clang__ */
 
 int perflib_roundrobin_affinity(affinity_t *cpu_set_bits, size_t cpu_set_size,
                                 size_t num, size_t cnt, void *arg)

to be honest I'm with Neal here. My reasoning is the peftools need to be portable to as many platforms/compilers as (conveniently) possible. you are rolling the builtin implementation anway so using a bultinn one here does not buy as much.

on the other hand if limit ourselves to clang and GCC tools, then I'm fine with going to bultin only one.

the true reason I don't like the if/else here is it leaves a dead/untested code behind. In my opinion the true choice here should be:

  • being portable, then roll your own
    or
  • let's rely on compiler then code will work on platforms where bultiin is provided

in my view the perftools are roll your own case.

Comment thread source/pkeyread.c
"\t-v verbose output, includes min, max, stddev, and median times\n"
"\t-T timeout for each test run in seconds, can be fractional"
"\t-T timeout for each test run in seconds, can be fractional\n"
"\t-b Set CPU affinity for the threads (in round robin fashion)\n"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand Nikola's question better now. and I think he is making a good point. let me ask the question different way: what is a difference between running the test using the command:

./pkeyread -f all -k all -b 16

and

taskset  0xffff ./pkeyread -f all -k all 16

If I understand things right, then th -b is a shortcut so people don't need to think of using a taskset(1) is my understanding correct?

@nhorman

nhorman commented Oct 17, 2025

Copy link
Copy Markdown
Contributor

I think I understand Nikola's question better now. and I think he is making a good point. let me ask the question different way: what is a difference between running the test using the command:

I think the difference between:

./pkeyread -f all -k all -b 16

and

taskset  0xffff ./pkeyread -f all -k all 16

Is that in the latter case we rely on the OS scheduler to place threads on unique cores.

In the former case thread 1 is guaranteed to have an affinity of 0x1, thread 2 an affinity of 0x2, thread 3 an affinity of 0x4, etc.

In the latter all threads can run on any ore in the affinity set. Will they likely be scheduled to unique cores? Probably. Are they guaranteed to be? No.

I guess the question to ask is "Does that matter to us?", and honestly, I'm not sure of the answer there.

@esyr

esyr commented Oct 17, 2025

Copy link
Copy Markdown
Member Author

The work is ok, but I'm a little bit lost why the work is needed.

So, the original reason I ended up writing that is that while working on x509storeissuer updates, I started seeing some anomalous results, and wanted to exclude that aspect from the list of possible factors. In general, pinning threads helps with the following:

  • it minimises noise from rescheduling and discrepancies of impacts of performance of specific CPU cores across test runs;
  • it allows referencing to thread numbers (which is sometimes useful in cases of anomalous performance of some of them), as they correlate with CPU cores that way;
  • it allows providing specific thread mappings on the system's topology, which is useful in conjunction with some other aspects of test runs, like, the way some resources are shared across threads or the way some thread perform work, and/or the CPU mask set for the whole test.

All those factors are predominantly relevant only when running on NUMA systems, naturally.

@Sashan

Sashan commented Oct 17, 2025

Copy link
Copy Markdown
Contributor
> All those factors are predominantly relevant only when running on NUMA systems, naturally.

understood. my preference here is to get away with taskset(1) (if possible) also it looks like windows offer similar mechanism according to stack overflow The takset seems to be available on FreeBSD. Solaris has prset(1M) to set affinity for process I believe other systems which can manage thread affinity expose their own command line tooling.

In my opinion the less we do here the better.

@Sashan Sashan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I'm not entirely convinced about affinity changes here. My preference is to get away with task_set (and similar command line tools on other than linux OSes if stach command is provided). Would you mind to keep it in your perftools fork for a while? I would include it when I will be sure perftools need it. At the moment I just think time has not come yet. thanks.

@Sashan

Sashan commented Oct 20, 2025

Copy link
Copy Markdown
Contributor
> > > All those factors are predominantly relevant only when running on NUMA systems, naturally.

back in the time you were hunting down the x509storeissuer performance would running the test using task_set help you or it was not sufficient so you had to opt to implement thread pinning using glibc?

Comment thread source/perflib/err.c
}

void
err(int status, const char *fmt, ...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow why we need to duplicate all this stuff for thread affinity.. This seems unrelated.

If we want this it should probably be done separately, or we should ask ourselves why we can't use the standard stuff.

Comment thread source/perflib/err.h
do { \
fprintf(stderr, "%s:%d(%s): ", __FILE__, __LINE__, __FUNCTION__); \
errx(__VA_ARGS__); \
} while (0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like yet another way to do this just for this PR, I'm questioning why this needs to be included,

Comment thread source/perflib/threads.c
static ossl_inline unsigned int popcount(affinity_t a)
{
return __builtin_popcountl(a);
}

@bob-beck bob-beck Oct 23, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what's the goal and the problem we are seeking to solve here that we need code to support to solve.

How will we support this, when people start fiddling with it and complain about impact because they do it poorly, and attempt to be smarter than the scheduler and do it badly.

To ask this another way, if our goal is to have less noisy data for falling-down-the-hill performance tests, does this belong in testing support for us and not in the main library?

mea culpa, that's what this is, so yeah it's in the test library, I have less objections.

@Sashan

Sashan commented Oct 23, 2025

Copy link
Copy Markdown
Contributor

IMO we should try to rely on using command line tools to deal with thread affinity (like taskset(1) and similar commands provided by platform where script is running.

so I would drop the affinity related changes from here. other pieces left behind still seem useful as they make things tidy.

@esyr

esyr commented Oct 30, 2025

Copy link
Copy Markdown
Member Author

As mentioned in [1], I haven't managed to find any discernible difference in performance and noise levels on pkeyread and x509storeissuer tests when thread pinning is applied, and, since there's strong hesitance towards having such a capability, I see no reason in pursuing the acceptance of this patch set.

[1] openssl/project#1693 (comment)

@esyr esyr closed this Oct 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[perftools] Add support for setting thread affinity in tests

6 participants